/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ts, ArkFile, AstTreeUtils } from "arkanalyzer/lib";
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { BaseChecker, BaseMetaData } from "../BaseChecker";
import { Defects, IssueReport } from "../../model/Defects";
import { FileMatcher, MatcherCallback, MatcherTypes } from "../../matcher/Matchers";
import { Rule } from "../../model/Rule";
import { RuleListUtil } from "../../utils/common/DefectsList";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'NoInvalidVoidTypeCheck');
const gMetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: "docs/no-invalid-void-type-check.md",
    description: 'Disallow `void` type outside of generic or return types',
    messages: {
        invalidVoidForGeneric:
            '{{ generic }} may not have void as a type argument.',
        invalidVoidNotReturn: 'void is only valid as a return type.',
        invalidVoidNotReturnOrGeneric:
            'void is only valid as a return type or generic type argument.',
        invalidVoidNotReturnOrThisParam:
            'void is only valid as return type or type of `this` parameter.',
        invalidVoidNotReturnOrThisParamOrGeneric:
            'void is only valid as a return type or generic type argument or the type of a `this` parameter.',
        invalidVoidUnionConstituent:
            'void is not valid as a constituent in a union type',
    },
    defaultOptions: { allowInGenericTypeArguments: true }
};

type Options = {
    allowAsThisParameter?: boolean;
    allowInGenericTypeArguments?: [string, ...string[]] | boolean;
}

const validFunctionParents: ts.SyntaxKind[] = [
    ts.SyntaxKind.FunctionDeclaration,
    ts.SyntaxKind.FunctionExpression,
    ts.SyntaxKind.ArrowFunction,
    ts.SyntaxKind.MethodDeclaration,
    ts.SyntaxKind.MethodSignature,
    ts.SyntaxKind.FunctionType
]

const invalidGrandParents: ts.SyntaxKind[] = [
    ts.SyntaxKind.PropertySignature,
    ts.SyntaxKind.CallExpression,
    ts.SyntaxKind.PropertyDeclaration,
    ts.SyntaxKind.Identifier
]

const validUnionMembers: ts.SyntaxKind[] = [
    ts.SyntaxKind.VoidKeyword,
    ts.SyntaxKind.NeverKeyword,

];

export class NoInvalidVoidTypeCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE,
    };

    public registerMatchers(): MatcherCallback[] {
        const matchFileCb: MatcherCallback = {
            matcher: this.fileMatcher,
            callback: this.check
        }
        return [matchFileCb];
    }

    checkNoInvalidVoidType(astTree: ts.SourceFile, option: Options): { line: number, colum: number, message: string, sourceCode: string, generic?: string }[] {
        let errorPositions: { line: number, colum: number, message: string, sourceCode: string, generic?: string }[] = [];
        let allowInGenericTypeArguments = option.allowInGenericTypeArguments;
        let allowAsThisParameter = option.allowAsThisParameter;

        function checkGenericTypeArgument(node: ts.Node) {
            if ((node.parent?.kind !== ts.SyntaxKind.TypeReference)
                || !isGenericArrayType(node.parent)) {
                return;
            }
            if (Array.isArray(allowInGenericTypeArguments)) {
                let childNode = findFirstIdentifierNode(node.parent) ?? node.parent.getChildAt(0);
                const fullyQualifiedName = childNode.getText().trim();
                if (!allowInGenericTypeArguments.map((argument: string) => argument.replace(/\s+/g, '')).includes(fullyQualifiedName)) {
                    let { line, character } = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart());
                    errorPositions.push({ line: line + 1, colum: character + 1, message: 'invalidVoidForGeneric', sourceCode: node.getText(), generic: fullyQualifiedName });
                }
                return;
            }
            if (!allowInGenericTypeArguments) {
                let { line, character } = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart());
                errorPositions.push({ line: line + 1, colum: character + 1, message: allowAsThisParameter ? 'invalidVoidNotReturnOrThisParam' : 'invalidVoidNotReturn', sourceCode: node.getText() });
            }
        }

        function isGenericArrayType(node: ts.Node): boolean {
            if (node.kind == ts.SyntaxKind.UnionType) {
                return node.getChildren().some(child => { return /<[^>]*\bvoid\b[^>]*>/.test(child.getText()) });
            }
            let nodeText = node.getText()
            return nodeText.includes('<') && nodeText.includes('>');
        }

        function findFirstIdentifierNode(parentNode: ts.Node): ts.Node | undefined {
            if (!parentNode || !parentNode.getChildren) {
                return undefined;
            }
            const children = parentNode.getChildren();
            for (const child of children) {
                if (child.kind === ts.SyntaxKind.Identifier) {
                    return child;
                }
            }
            return undefined;
        }

        function isValidUnionType(node: ts.UnionTypeNode): boolean {
            return node.types.every(member => {
                return validUnionMembers.includes(member.kind) ||
                    (member.kind === ts.SyntaxKind.TypeReference && isGenericArrayType(member.parent) &&
                        member.getChildren().some(childNode => {
                            return childNode.getText().includes('void');
                        }));
            });
        }

        function getNotReturnOrGenericMessageId(node: ts.Node): string {
            return node.parent?.kind === ts.SyntaxKind.UnionType ?
                'invalidVoidUnionConstituent' : 'invalidVoidNotReturnOrGeneric';
        }

        function checkNode(node: ts.Node) {
            if (node.kind === ts.SyntaxKind.VoidKeyword) {
                if (node.parent?.kind === ts.SyntaxKind.TypeReference &&
                    isGenericArrayType(node.parent)) {
                    checkGenericTypeArgument(node);
                    return;
                }

                if (node.parent?.kind === ts.SyntaxKind.UnionType && isValidUnionType(node.parent as ts.UnionTypeNode)) {
                    return;
                }

                if (node.parent?.kind === ts.SyntaxKind.TypeParameter) {
                    let parentNode = node.parent as ts.TypeParameterDeclaration;
                    if (node.parent.getChildren().some(childNode => {
                        return childNode.kind == ts.SyntaxKind.EqualsToken;
                    })) {
                        if (parentNode.default != node) {
                            let { line, character } = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart());
                            errorPositions.push({ line: line + 1, colum: character + 1, message: getNotReturnOrGenericMessageId(node), sourceCode: node.getText() });
                        }
                        return;
                    }
                }

                if (allowAsThisParameter &&
                    node.parent?.kind === ts.SyntaxKind.Parameter &&
                    node.parent?.getChildAt(0).getText() === "this") {
                    return;
                }

                let vaildValue = allowInGenericTypeArguments === true ?
                    validFunctionParents.includes(node.parent?.kind) ||
                    isGenericArrayType(node.parent) :
                    validFunctionParents.includes(node.parent?.kind);
                if (vaildValue && !invalidGrandParents.includes(node.parent?.kind)) {
                    return;
                }

                let { line, character } = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart());
                let messageId =
                    allowInGenericTypeArguments && allowAsThisParameter ?
                        'invalidVoidNotReturnOrThisParamOrGeneric' : allowInGenericTypeArguments ?
                            getNotReturnOrGenericMessageId(node) : allowAsThisParameter ?
                                'invalidVoidNotReturnOrThisParam' : 'invalidVoidNotReturn';
                errorPositions.push({ line: line + 1, colum: character + 1, message: messageId, sourceCode: node.getText() });

            }
            node.forEachChild(checkNode)
        }
        checkNode(astTree);
        return errorPositions;
    }

    private getOption(): Options {
        let option = this.metaData.defaultOptions;
        if (this.rule && this.rule.option[0]) {
            option = this.rule.option[0] as Options;
        }
        return option;
    }

    public check = (target: ArkFile) => {
        let astTree = AstTreeUtils.getASTNode(target.getName(), target.getCode());
        let options = this.getOption();
        let errorPositions: { line: number, colum: number, message: string, sourceCode: string, generic?: string }[] = this.checkNoInvalidVoidType(astTree, options);
        errorPositions.forEach(position => {
            this.addIssueReport(target, position.line, position.colum, position.sourceCode, position.message, position.generic);
        });
    }

    private addIssueReport(arkFile: ArkFile, lineNum: number, startColum: number, code: string, messageId: string, generic: string = '') {
        const severity = this.rule.alert ?? this.metaData.severity;
        let message = messageId !== 'invalidVoidForGeneric' ? this.metaData.messages[messageId] : `${generic} may not have void as a type argument.`;
        let filePath = arkFile.getFilePath();
        let endColum = startColum + code.length - 1;
        const fixKey = lineNum + '%' + startColum + '%' + endColum + '%' + this.rule.ruleId;
        const mergeKey = filePath + '%' + fixKey + '%' + message;
        let defect = new Defects(lineNum, startColum, message, severity, this.rule.ruleId, mergeKey, this.metaData.ruleDocPath, true, false, false, fixKey);
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }
}


